Custom Visual Example - Aster Plot
The following examples shows several of the key aspects of using the CV API (2.0) through the Aster Plot example found in the Marketplace inside the application.
Code Break Down
Main Function
The main function (row 1) is the key entry point.
- It registers a listener for the render event, calling the render function on row 6
- It sets a series of custom CSS styles - which calls another function on row 143
Render Function
Key aspects of the graphic wire up include:
- The render function (row 6), which is called as each chart is rendered in the client, starts by getting access to the current data set (on row 8) from the trellis function in the data object.
- within the data set, the list of data points in the query result are accessed on row 9
- The parent object that will house the drawing is found on row 10 from the canvas object.
- the data points from row 9 are processed in rows 27-39, where the numeric values behind each data point are extracted and injected into an array "visualModel", which was substantiated on row 22.
- sizing for the visual is calculated based on the canvas size in rows 41-44
- a classic D3 pie visualization is setup and drawn in rows 47 - 73.
- On rows 75-78, the visualModel items are pushed into the graphic objects
- The merging of the data points with the D3 graphic logic is resolved in 82-101. Here, the data points are used to drive colors, tooltips (for mouse over events); triggering of the Pyramid context menus; and the selection state.
function main() {
cvApi2.canvas.setCustomCssStyle(defineStyles());
cvApi2.canvas.addEventListener(cvApi2.enums.events.Render, render);
}
function render() {
//get the trellised data set for rendering
var currentTrellisedData = cvApi2.resultSet.data.getCurrentTrellisData();
var dataPoints = currentTrellisedData.datapoints
var element = cvApi2.canvas.getHTMLElement();
var d3 = cvApi2.externalLibraries.d3;
//get value range (min and max value from the given array)
var valueRange = d3.array.extent(
dataPoints.map(function (dp) { return dp.numerics.value.rawValue }));
//define the scale
var scoreScale = d3.scale.scaleLinear()
.domain(valueRange).range([20, 100]);
var visualModel = {
items: [],
};
//get data points as object for d3
for (var i = 0; i < dataPoints.length; i++) {
var dp = dataPoints[i];
var dpValue = dp.numerics.value.rawValue;
visualModel.items.push({
datapoint: dp,
value: dpValue,
color: dp.numerics.color?.[0]?.formattedValue,
weight: dp.numerics.size?.[0]?.rawValue,
score: scoreScale(dpValue),
width: dp.numerics.size?.[0]?.rawValue,
label: dp.numerics.value.formattedValue,
});
}
var width = cvApi2.canvas.width;
var height = cvApi2.canvas.height;
var radius = Math.min(width, height) / 2;
var innerRadius = 0.40 * radius;
//pie based visual
var pie = d3.shape.pie()
.sort(null)
.value(function (d) { return d.width; });
//calculate arcs
var arc = d3.shape.arc()
.innerRadius(innerRadius)
.outerRadius(function (d) {
return (radius - innerRadius) * (d.data.score / 100.0) + innerRadius;
});
var outlineArc = d3.shape.arc()
.innerRadius(innerRadius)
.outerRadius(radius);
//select the SVG element
var root = d3.selection
.select(element)
.selectAll('g.root')
.data(function (d) { return [d]; });
root = root
.enter()
.append("g")
.attr('class', 'root')
.merge(root)
.attr('transform', 'translate(' + (width / 2) + ',' + (height / 2) + ')');
//solid arc appearance
var path = root
.selectAll(".solid-arc")
.data(pie(visualModel.items));
path.exit().remove();
path = path
.enter()
.append("path")
.attr("stroke", "#d3d3d3")
.merge(path)
.attr("fill", function (d) { return d.data.color; })
.attr("d", arc)
.attr("class", function (d) {
var className = 'solid-arc';
if (cvApi2.canvas.isSelectionEnabled()) {
if (d.data.datapoint.isSelected)
className += ' selected'
else className += ' non-selected'
}
return className;
})
.on("mouseover", function (d) { d.data.datapoint.showTooltip(d3.selection.event); })
.on("mouseout", function (d) { d.data.datapoint.hideTooltip(d3.selection.event); })
.on("contextmenu", function (d) { d.data.datapoint.showContextMenu(d3.selection.event); })
.on("click", function (d) { d.data.datapoint.select(); })
//outer line appearance
var outerPath = root
.selectAll(".outline-arc")
.data(pie(visualModel.items));
outerPath.exit().remove();
outerPath = outerPath
.enter()
.append("path")
.attr("fill", "none")
.attr("stroke", "#d3d3d3")
.attr("class", "outline-arc")
.merge(outerPath)
.attr("d", outlineArc);
// calculate the weighted mean score
var score =
visualModel.items.reduce(function (a, b) {
return a + (b.score * b.weight);
}, 0) /
visualModel.items.reduce(function (a, b) {
return a + b.weight;
}, 0);
//text appearance
var text = root
.selectAll('text')
.data(function (d) { return [d]; });
text = text
.enter()
.append("text")
.attr("class", "aster-score")
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.merge(text)
.text(function () { return isNaN(score) ? '-' : Math.round(score) });
}
// this function should return css string (if any) for custom style
function defineStyles(){
return '.axis path,'+
'.axis line {'+
'fill: none;'+
'stroke: #000;'+
'shape-rendering: crispEdges;}'+
'.bar {'+
'fill: orange;}'+
'.solid-arc:hover {'+
'stroke: black ;'+
'stroke-width:1.5px;}'+
'.solid-arc {'+
'-moz-transition: all 0.3s;'+
'-o-transition: all 0.3s;'+
'-webkit-transition: all 0.3s;'+
'transition: all 0.3s;}'+
'.solid-arc.selected {'+
'opacity: 1;}'+
'.solid-arc.non-selected {'+
'opacity: 0.5;}'+
'.x.axis path {'+
'display: none;}'+
'.aster-score { '+
'line-height: 1;'+
'font-weight: bold;'+
'font-size: 500%;}';
}